Item8-别让异常逃离析构函数

析构函数中发生异常是件棘手的事

由于析构函数常常被自动调用,在析构函数中抛出的异常往往会难以捕获,引发程序非正常退出或未定义行为

class Widget {
public:
  ...
  ~Widget() { ... }              // assume this might emit an exception
};

void doSomething()
{
  std::vector<Widget> v;
  ...
}     

当 v 被析构时,它有责任析构它包含的所有 Widgets。假设 v 中有十个 Widgets,在第一个的析构过程中,抛出一个异常。其它 9 个 Widgets 仍然必须被析构,否则它们持有的所有资源将被泄漏。这时如果第二个 Widget 析构又抛出异常,现在有两个同时活动的异常,程序若不是结束执行就是引发未定义行为。

假设使用一个类负责数据库连接:

class DBConnection {
public:
  ...
  static DBConnection create();   
  void close();                       
};              

为了确保客户不会忘记在对象上调用 close,一个合理的主意是为 DBConnection 建立一个资源管理类,在它的析构函数中调用 close。

class DBConn {               
public:                                
  ...
  ~DBConn()                          
  {                                   
   db.close();
  }
private:
  DBConnection db;
};

使用时:

{                                      
   DBConn dbc(DBConnection::create()); 
   ...                                   
}                                                      

如果 DBConn 析构函数调用close导致异常,则析构函数就会传播该异常,也就是允许异常离开这个析构函数,这会造成麻烦。

析构函数中处理异常的两种思路

有两个办法可以避免这一问题,DBConn的析构函数可以:

  • 如果close抛出异常就结束程序,通常通过调用 abort完成:

    DBConn::~DBConn()
    {
     try { db.close(); }
     catch (...) {
       make log entry that the call to close failed;
       std::abort();
     }
    }
    
  • 吞下因调用close而发生的异常:

    DBConn::~DBConn()
    {
     try { db.close(); }
     catch (...) {
          make log entry that the call to close failed;
     }
    }
    

如果程序遭遇一个“于析构期间发生的错误”后无法继续执行,“强迫结束程序”是个合理的选项,毕竟它可以阻止异常从析构函数传播出去从而导致未定义行为。

一般而言,吞掉异常是个坏主意,因为它压制了“某些动作失败”的重要信息,然而有些时候吞下异常也比负担“草率结束程序”或“不明确行为带来的风险”好。

提供类用户异常处理接口

在遭遇并忽略了一个错误后,程序必须能够继续可靠地执行,这才是一个可行的方案。

一个极佳的策略是重新设计 DBConn的接口,使其客户有机会对可能出现的问题作出反应。

class DBConn {
public:
  ...
  void close() //@ new function for client use
  {                                              
    db.close();
    closed = true;
  }

  ~DBConn()
  {
   if (!closed) {
   try {             //@ close the connection if the client didn't
     db.close();                                    
   }
   catch (...) {             //@ if closing fails,note that and terminate or swallow
     make log entry that call to close failed;      
     ...                                 
   }
  }

private:
  DBConnection db;
  bool closed;
};

将调用 close 的责任从析构函数移交给 DBConn 的客户,同时在 DBConn 的析构函数中包含一个“候补”调用。

如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。因为析构函数吐出异常就是危险,总会带来“过早结束程序”或“发生不明确行为”的风险。所以,让客户自己调用 close 并不是对他们带来负担,而是给他们一个处理错误的机会,否则他们没有机会响应。

总结

  • 析构函数绝对不要吐出异常。如果析构函数调用了可能抛出异常的函数,析构函数应该捕捉所有异常,然后吞下它们(不传播)或者结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通的函数(而非析构函数)执行该操作。